package com.ruoyi.web.utils.qg_pay.client;

import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.web.enums.HttpMethodEnum;
import com.ruoyi.web.utils.CommonUtil;
import com.ruoyi.web.utils.ReflectUtil;
import com.ruoyi.web.utils.qg_pay.anno.QGPayHttpConfig;
import com.ruoyi.web.utils.qg_pay.config.QGPayConfig;
import com.ruoyi.web.utils.qg_pay.entity.QGPayCommonRequestParam;
import com.ruoyi.web.utils.qg_pay.entity.QGPayCommonResponse;
import com.ruoyi.web.utils.qg_pay.entity.QueryParamMap;
import com.ruoyi.web.utils.qg_pay.enums.QGPayParamEnum;
import com.ruoyi.web.utils.qg_pay.exception.APIException;
import com.ruoyi.web.utils.qg_pay.utils.RsaUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.apache.logging.log4j.util.Strings;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

/**
 * http请求工具类
 *
 * @since 2023/1/12 19:06
 */
@Slf4j
@Component
@AllArgsConstructor
public class QGPayHttpClient {

    private final CommonUtil commonUtil;
    private final QGPayConfig qgConfig;

    /**
     * 发起请求
     *
     * @param request 请求参数
     * @return {@link QGPayCommonResponse}
     * @since 2023/3/13 15:40
     */
    public QGPayCommonResponse request(QGPayCommonRequestParam<?, ?> request) {
        Class<? extends QGPayCommonRequestParam> requestClass = request.getClass();
        QGPayHttpConfig httpConfig = requestClass.getAnnotation(QGPayHttpConfig.class);

        // 将可能存在的url参数转化为QueryParamMap，并添加固定url参数
        QueryParamMap queryParamMap = getQueryParamMap(request);
        // 如果需要签名，进行签名
        signature(httpConfig, request, queryParamMap);
        // 拼接url
        String url = getUrl(httpConfig, queryParamMap);

        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;
        try {
            SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(
                    SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
                    NoopHostnameVerifier.INSTANCE
            );
            httpClient = HttpClients.custom().setSSLSocketFactory(factory).build();
            HttpRequestBase http = getHttpRequest(httpConfig.method(), url, request);
            // 处理请求头
            initHeader(http, request);
            // 设置连接属性
            http.setConfig(getRequestConfig());
            // 执行请求
            response = httpClient.execute(http);
            // 处理响应结果并返回
            return handleResponse(httpConfig, response);
        } catch (IOException | RuntimeException e) {
            String errorMsg = String.format("%s请求失败，url:[%s]\n异常信息：%s",
                    httpConfig.method(),
                    url,
                    e.getMessage());
            throw new APIException(errorMsg);
        } catch (Throwable e) {
            throw new APIException(e);
        } finally {
            // 关闭资源
            if (!Objects.isNull(response)) {
                try {
                    response.close();
                } catch (IOException e) {
                    log.error("response流关闭异常：{}", e.getMessage());
                }
            }
            if (!Objects.isNull(httpClient)) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    log.error("httpClient流关闭异常：{}", e.getMessage());
                }
            }
        }
    }

    /**
     * 处理返回结果
     *
     * @param httpConfig 接口信息
     * @param response   返回结果
     * @return {@link QGPayCommonResponse <?>}
     * @since 2023/1/19 15:39
     */
    private QGPayCommonResponse handleResponse(QGPayHttpConfig httpConfig, CloseableHttpResponse response) {
        // 判断网络请求是否成功
        int statusCode = response.getStatusLine().getStatusCode();
        String resStr = getResponseEntityStr(response);

        // 请求成功
        if (HttpStatus.SC_OK == statusCode) {
            QGPayCommonResponse res = commonUtil.parseJson(resStr, QGPayCommonResponse.class);
            res.setIsSuccess(res.getCode() == 0);

            // 如果有响应数据
            if (!Objects.isNull(res.getData())) {
                String dataJson = commonUtil.toJson(res.getData());
                Object data = commonUtil.parseJson(dataJson, httpConfig.responseType());
                res.setData(data);
            }

            return res;
        }

        // 请求失败
        String errorMsg = String.format("请求失败，状态码：%s，\n返回结果：%s",
                statusCode,
                resStr);
        throw new APIException(errorMsg);
    }

    /**
     * 获取响应体内容
     *
     * @param response 响应包
     * @return {@link String}
     * @since 2023/3/8 09:26
     */
    private String getResponseEntityStr(CloseableHttpResponse response) {
        HttpEntity entity = response.getEntity();
        try {
            return EntityUtils.toString(entity, "utf-8");
        } catch (IOException e) {
            throw new APIException("获取返回结果失败", e);
        }
    }

    /**
     * 初始化请求头
     *
     * @param http    http请求
     * @param request 请求参数
     * @since 2023/1/19 15:27
     */
    private void initHeader(HttpRequestBase http, QGPayCommonRequestParam<?, ?> request) {
        // 构造请求头
        http.setHeader("Content-type", "application/json; charset=utf-8");
        // http.setHeader("Connection", "keep-alive");
    }

    /**
     * 根据请求方式创建对应的请求
     *
     * @param method 请求方式
     * @param url    url
     * @return {@link HttpRequestBase}
     * @since 2023/1/19 15:21
     */
    private HttpRequestBase getHttpRequest(HttpMethodEnum method, String url, QGPayCommonRequestParam<?, ?> request) {
        log.info("{}请求url:[{}]", method, url);

        if (HttpMethodEnum.GET.equals(method)) {
            return new HttpGet(url);

        } else if (HttpMethodEnum.POST.equals(method)) {
            String jsonString = commonUtil.toJson(request.getBody());
            Map<String, Object> hashMap = commonUtil.parseJson(jsonString, Map.class);
            clearEmptyParam(hashMap);
            StringEntity entity = new StringEntity(
                    commonUtil.toJson(hashMap)
                    , Consts.UTF_8
            );
            HttpPost post = new HttpPost(url);
            post.setEntity(entity);
            return post;
        } else if (HttpMethodEnum.DELETE.equals(method)) {
            return new HttpDelete(url);

        } else if (HttpMethodEnum.PUT.equals(method)) {
            String jsonString = JSONObject.toJSONString(request.getBody());
            Map<String, Object> hashMap = commonUtil.parseJson(jsonString, Map.class);
            clearEmptyParam(hashMap);
            StringEntity entity = new StringEntity(
                    JSONObject.toJSONString(hashMap)
                    , Consts.UTF_8
            );
            HttpPut put = new HttpPut(url);
            put.setEntity(entity);
            return put;
        } else {
            throw new APIException("不支持的请求方式：" + method);
        }
    }

    /**
     * 获取连接属性设置
     *
     * @return {@link RequestConfig}
     * @since 2023/1/19 15:29
     */
    private static RequestConfig getRequestConfig() {
        // 设置 连接 属性
        RequestConfig requestConfig = RequestConfig.custom()
                // 请求时间
                .setSocketTimeout(30000)
                // 超时时间
                .setConnectTimeout(30000)
                .setConnectionRequestTimeout(30000)
                // 是否跳转链接(反向代理)
                .setRedirectsEnabled(false)
                .build();
        return requestConfig;
    }

    /**
     * 拼接url
     *
     * @param httpConfig    接口信息
     * @param queryParamMap url参数
     * @return {@link String}
     * @since 2023/1/17 19:11
     */
    private String getUrl(QGPayHttpConfig httpConfig, QueryParamMap queryParamMap) {
        return new StringBuilder(qgConfig.getUrl())
                .append(httpConfig.url())
                .append(queryParamMap.transferUrlParam())
                .toString();
    }

    /**
     * 签名
     *
     * @param httpConfig    接口信息
     * @param request       请求信息
     * @param queryParamMap url参数
     * @since 2023/1/17 19:32
     */
    private void signature(QGPayHttpConfig httpConfig,
                           QGPayCommonRequestParam<?, ?> request,
                           QueryParamMap queryParamMap) {
        // 接口不需要签名
        if (!httpConfig.needSignature()) {
            return;
        }

        if (Strings.isBlank(qgConfig.getPublicKey())
                || Strings.isBlank(qgConfig.getPrivateKey())) {
            throw new APIException("参数异常，需要签名的接口没有正确传入密钥对信息");
        }

        Map<String, Object> treeMap = new TreeMap<>(Comparator.naturalOrder());
        treeMap.putAll(queryParamMap);

        // POST/PUT请求
        if (HttpMethodEnum.POST.equals(httpConfig.method())
                || HttpMethodEnum.PUT.equals(httpConfig.method())) {
            // 将请求体的参数添加到treeMap中
            Object body = request.getBody();
            if (!Objects.isNull(body)) {
                String json = commonUtil.toJson(body);
                HashMap<String, Object> bodyMap = commonUtil.parseJson(json, HashMap.class);
                // 清除为空的参数
                clearEmptyParam(bodyMap);
                treeMap.putAll(bodyMap);
            }
        }

        // 没有参数，抛出异常
        if (CollectionUtils.isEmpty(treeMap)) {
            throw new APIException("参数异常，没有参数");
        }

        // 生成签名
        String encryptSrc = getEncryptSrc(treeMap);
        String signature = RsaUtil.encryptByPrivate(encryptSrc, qgConfig.getPrivateKey());
        // 设置签名属性
        setSignature(request, signature);
    }

    /**
     * 设置签名属性
     *
     * @param request   请求信息
     * @param signature 签名
     * @since 2023/3/14 14:38
     */
    private void setSignature(QGPayCommonRequestParam<?, ?> request,
                              String signature) {
        Object body = request.getBody();
        Method setter = ReflectUtil.getSetter(body, QGPayParamEnum.SIGN.getName(), String.class);
        if (Objects.isNull(setter)) {
            throw new APIException("设置签名属性失败");
        }

        try {
            setter.invoke(body, signature);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new APIException("设置签名属性失败");
        }
    }

    /**
     * 清除空参数
     *
     * @param map 参数
     * @since 2023/1/19 15:02
     */
    public static void clearEmptyParam(Map<String, Object> map) {
        List<String> clearList = new ArrayList<>();
        map.forEach((k, v) -> {
            if (Objects.isNull(v)) {
                clearList.add(k);
            }
        });

        clearList.forEach(map::remove);
    }

    /**
     * 将可能存在的url参数转化为QueryParamMap，并添加固定url参数
     *
     * @param request 请求信息
     * @return {@link QueryParamMap}
     * @since 2023/1/17 19:10
     */
    private QueryParamMap getQueryParamMap(QGPayCommonRequestParam<?, ?> request) {
        return Objects.isNull(request.getQueryParam()) ?
                new QueryParamMap()
                : QueryParamMap.convertEntity(request.getQueryParam());
    }

    /**
     * 根据排序后的参数集合，生成加密所需的源字符串
     *
     * @param map 排序后的参数集合
     * @return {@link String}
     * @since 2023/3/13 16:20
     */
    public static String getEncryptSrc(Map<String, Object> map) {
        StringBuilder builder = new StringBuilder();

        map.forEach((k, v) -> builder.append(k)
                .append("=")
                .append(v)
                .append("&"));

        String str = builder.toString();
        return str.substring(0, str.length() - 1);
    }
}
